// JsonUtil
// 处理 js json 字符串序列化时数字精度丢失问题
// @author https://github.com/sidorares/json-bigint.git
// @modify yxk
// @description 原json-bigint库中会污染 Object.hasOwnProperty 方法，在这里重新调整
// (c) BSD-3-Clause
// https://github.com/fastify/secure-json-parse/graphs/contributors and https://github.com/hapijs/bourne/graphs/contributors

const suspectProtoRx =
  /(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/
const suspectConstructorRx =
  /(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/
const maxNumber = Math.pow(2, 53)
const escapee = {
  '"': '"',
  '\\': '\\',
  '/': '/',
  b: '\b',
  f: '\f',
  n: '\n',
  r: '\r',
  t: '\t'
}

export default class JsonUtil {
  options = {
    strict: false, // not being strict means do not generate syntax errors for "duplicate key"
    storeAsString: false, // toggles whether the values should be stored as BigNumber (default) or a string
    alwaysParseAsBig: false, // toggles whether all numbers should be Big
    useNativeBigInt: false, // toggles whether to use native BigInt instead of bignumber.js
    protoAction: 'error',
    constructorAction: 'error'
  }

  ch
  text
  at

  constructor(opts) {
    // If there are options, then use them to override the default _options
    if (opts) {
      const arr = ['error', 'ignore', 'preserve']
      if (opts.constructorAction) {
        if (!arr.includes(opts.constructorAction)) {
          throw new Error(
            `Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed ${opts.constructorAction}`
          )
        }
      }
      if (opts.protoAction) {
        if (!arr.includes(opts.protoAction)) {
          throw new Error(
            `Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed ${opts.protoAction}`
          )
        }
      }
      this.options = Object.assign(this.options, opts)
    }
  }

  errorFunc(m) {
    // eslint-disable-next-line no-throw-literal
    throw {
      name: 'SyntaxError',
      message: m,
      at: this.at,
      text: this.text
    }
  }

  nextFunc(c) {
    // If a c parameter is provided, verify that it matches the current character.

    if (c && c !== this.ch) {
      this.errorFunc("Expected '" + c + "' instead of '" + this.ch + "'")
    }

    // Get the next character. When there are no more characters,
    // return the empty string.

    this.ch = this.text.charAt(this.at)
    this.at += 1
    return this.ch
  }

  numberFunc() {
    // Parse a number value.

    let str = ''

    if (this.ch === '-') {
      str = '-'
      this.nextFunc('-')
    }
    // eslint-disable-next-line no-unmodified-loop-condition
    while (this.ch >= '0' && this.ch <= '9') {
      str += this.ch
      this.nextFunc()
    }
    if (this.ch === '.') {
      str += '.'
      // eslint-disable-next-line no-unmodified-loop-condition
      while (this.nextFunc() && this.ch >= '0' && this.ch <= '9') {
        str += this.ch
      }
    }
    if (this.ch === 'e' || this.ch === 'E') {
      str += this.ch
      this.nextFunc()
      if (this.ch === '-' || this.ch === '+') {
        str += this.ch
        this.nextFunc()
      }
      // eslint-disable-next-line no-unmodified-loop-condition
      while (this.ch >= '0' && this.ch <= '9') {
        str += this.ch
        this.nextFunc()
      }
    }

    const num = +str
    if (!isFinite(num)) {
      this.errorFunc('Bad number')
    } else {
      return num >= maxNumber ? str : num
    }
  }

  stringFunc() {
    // Parse a string value.

    let hex
    let i
    let str = ''
    let uffff

    // When parsing for string values, we must look for " and \ characters.

    if (this.ch === '"') {
      let startAt = this.at
      while (this.nextFunc()) {
        if (this.ch === '"') {
          if (this.at - 1 > startAt)
            str += this.text.substring(startAt, this.at - 1)
          this.nextFunc()
          return str
        }
        if (this.ch === '\\') {
          if (this.at - 1 > startAt)
            str += this.text.substring(startAt, this.at - 1)
          this.nextFunc()
          if (this.ch === 'u') {
            uffff = 0
            for (i = 0; i < 4; i += 1) {
              hex = parseInt(this.nextFunc(), 16)
              if (!isFinite(hex)) {
                break
              }
              uffff = uffff * 16 + hex
            }
            str += String.fromCharCode(uffff)
          } else if (typeof escapee[this.ch] === 'string') {
            str += escapee[this.ch]
          } else {
            break
          }
          startAt = this.at
        }
      }
    }
    this.errorFunc('Bad string')
  }

  whiteFunc() {
    // eslint-disable-next-line no-unmodified-loop-condition
    while (this.ch && this.ch <= ' ') {
      this.nextFunc()
    }
  }

  wordFunc() {
    // true, false, or null.

    switch (this.ch) {
      case 't':
        this.nextFunc('t')
        this.nextFunc('r')
        this.nextFunc('u')
        this.nextFunc('e')
        return true
      case 'f':
        this.nextFunc('f')
        this.nextFunc('a')
        this.nextFunc('l')
        this.nextFunc('s')
        this.nextFunc('e')
        return false
      case 'n':
        this.nextFunc('n')
        this.nextFunc('u')
        this.nextFunc('l')
        this.nextFunc('l')
        return null
    }
    this.errorFunc("Unexpected '" + this.ch + "'")
  }

  arrayFunc() {
    // Parse an array value.

    const arr = []

    if (this.ch === '[') {
      this.nextFunc('[')
      this.whiteFunc()
      if (this.ch === ']') {
        this.nextFunc(']')
        return arr // empty array
      }
      // eslint-disable-next-line no-unmodified-loop-condition
      while (this.ch) {
        arr.push(this.valueFunc())
        this.whiteFunc()
        if (this.ch === ']') {
          this.nextFunc(']')
          return arr
        }
        this.nextFunc(',')
        this.whiteFunc()
      }
    }
    this.errorFunc('Bad array')
  }

  objectFunc() {
    // Parse an object value.

    let key
    const obj = {}

    if (this.ch === '{') {
      this.nextFunc('{')
      this.whiteFunc()
      if (this.ch === '}') {
        this.nextFunc('}')
        return obj // empty object
      }
      // eslint-disable-next-line no-unmodified-loop-condition
      while (this.ch) {
        key = this.stringFunc()
        this.whiteFunc()
        this.nextFunc(':')
        // if (
        //   this.options.strict === true &&
        //   Object.hasOwnProperty.call(obj, key)
        // ) {
        //   this.errorFunc('Duplicate key "' + key + '"')
        // }

        if (suspectProtoRx.test(key) === true) {
          if (this.options.protoAction === 'error') {
            this.errorFunc('Object contains forbidden prototype property')
          } else if (this.options.protoAction === 'ignore') {
            this.valueFunc()
          } else {
            obj[key] = this.valueFunc()
          }
        } else if (suspectConstructorRx.test(key) === true) {
          if (this.options.constructorAction === 'error') {
            this.errorFunc('Object contains forbidden constructor property')
          } else if (this.options.constructorAction === 'ignore') {
            this.valueFunc()
          } else {
            obj[key] = this.valueFunc()
          }
        } else {
          obj[key] = this.valueFunc()
        }

        this.whiteFunc()
        if (this.ch === '}') {
          this.nextFunc('}')
          return obj
        }
        this.nextFunc(',')
        this.whiteFunc()
      }
    }
    this.errorFunc('Bad object')
  }

  valueFunc() {
    // Parse a JSON value. It could be an object, an array, a string, a number,
    // or a word.

    this.whiteFunc()
    switch (this.ch) {
      case '{':
        return this.objectFunc()
      case '[':
        return this.arrayFunc()
      case '"':
        return this.stringFunc()
      case '-':
        return this.numberFunc()
      default:
        return this.ch >= '0' && this.ch <= '9'
          ? this.numberFunc()
          : this.wordFunc()
    }
  }

  parse(source, reviver) {
    this.text = source + ''
    this.at = 0
    this.ch = ' '

    const result = this.valueFunc()
    this.whiteFunc()
    if (this.ch) {
      this.errorFunc('Syntax error')
    }

    return typeof reviver === 'function'
      ? (function walkFunc(holder, key) {
          // eslint-disable-next-line no-unused-vars
          let k
          let v
          const value = holder[key]
          if (value && typeof value === 'object') {
            Object.keys(value).forEach(function (k) {
              v = walkFunc(value, k)
              if (v !== undefined) {
                value[k] = v
              } else {
                delete value[k]
              }
            })
          }
          return reviver.call(holder, key, value)
        })({ '': result }, '')
      : result
  }
}
